#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# File              : pyHtmlPreviewer.py
# Author            : yang <mightyang@hotmail.com>
# Date              : 18.01.2020
# Last Modified Date: 18.01.2020
# Last Modified By  : yang <mightyang@hotmail.com>

from PySide2 import QtGui, QtWidgets, QtCore, QtWebEngineWidgets, QtWebChannel, QtXml
from html.parser import HTMLParser
import os, base64, re, vim



class browserMainWindow(QtWidgets.QWidget):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.resize(1500,800)
        self.actions={}
        self.restoreSize = self.geometry()
        self.follow = False
        self.initMain()

    def initMain(self):
        mainLayout = QtWidgets.QVBoxLayout(self)
        self.setLayout(mainLayout)

        self.toolBar = QtWidgets.QToolBar(self)
        self.actions['pageEditable'] = self.toolBar.addAction('编辑模式')
        self.actions['pageEditable'].setCheckable(True)
        self.actions['elementMask'] = self.toolBar.addAction('标记元素')
        self.actions['elementMask'].setCheckable(True)


        pyHtmlViewer = QtWebEngineWidgets.QWebEngineView(self)
        self.pyHtmlPage = pyHtmlBrowserPage(QtWebEngineWidgets.QWebEngineProfile('pyHtmlViewer', pyHtmlViewer), pyHtmlViewer)
        pyHtmlViewer.setPage(self.pyHtmlPage)

        mainLayout.addWidget(self.toolBar)
        mainLayout.addWidget(pyHtmlViewer)

    def refreshHtml(self, target='VIM', p=None):
        self.pyHtmlPage.refreshHtmlSignal.emit(target, p)

    def switchVisible(self):
        if self.isVisible():
            self.restoreSize = self.geometry()
            self.hide()
        else:
            self.refreshHtml('BROWSER')
            self.show()
            self.setGeometry(self.restoreSize)

    def hideEvent(self, event):
        self.restoreSize = self.geometry()

    def closeBrowser(self):
        self.follow = False
        self.close()
        QtCore.QCoreApplication.instance().exit()


class pyHtmlParser(HTMLParser):
    def __init__(self, posAtText=True):
        super().__init__()
        self.data=''
        self.posAtText=posAtText
        self.addPosTag = ['a', 'p', 'b', 'li', 'img', 'h1', 'h2', 'h3', 'h4', 'input', 'div', 'title', 'body', 'td', 'span']

    def addPosToClass(self, tag, attrs, offset=0):
        if tag in self.addPosTag:
            r,c = self.getpos()
            attrs.append(('data-pyhtmlpos', '{},{}'.format(r, c+offset-1)))
        return attrs

    def handle_starttag(self, tag, attrs):
        offset=0
        if self.posAtText:
            offset = len('<' + tag + ''.join([' {}="{}"'.format(name, value) for name, value in attrs]) + ' >')
        newAttrs = self.addPosToClass(tag, attrs, offset)
        self.data += '<' + tag + ''.join([' {}="{}"'.format(name, value) for name, value in newAttrs]) + ' >'
        #  print(self.get_starttag_text())

    def handle_endtag(self, tag):
        self.data += '</{}>'.format(tag)

    def handle_startendtag(self, tag, attrs):
        attrs = self.addPosToClass(tag, attrs)
        self.data += '<' + tag + ''.join([' {}="{}"'.format(name, value) for name, value in attrs]) + ' />'

    def handle_data(self, data):
        self.data += data

    def handle_comment(self, data):
        self.data += '<!--{}-->'.format(data)

    def handle_decl(self, decl):
        self.data += '<!{}>'.format(decl)

    def unknown_decl(self, data):
        self.data += data


class pyHtmlBrowserPage(QtWebEngineWidgets.QWebEnginePage):
    pageEditableSignal = QtCore.Signal(str)
    elementMaskSignal = QtCore.Signal(str)
    refreshHtmlSignal = QtCore.Signal(str, str)
    def __init__(self, profile, parent=None):
        super().__init__(profile, parent)
        self.objs = {}
        self.scriptPath = vim.eval('s:scriptPath')
        self.initObjs()
        self.initChannel()
        self.widget = parent.parent()
        self.loadFinished.connect(lambda: self.widget.setWindowTitle(self.title()))
        self.loadFinished.connect(self.onLoadFinished)
        self.initJS()
        self.connections()
        self.refreshHtmlSignal.emit('BROWSER', '')

    def connections(self):
        self.widget.actions['pageEditable'].triggered.connect(lambda:self.pageEditableSignal.emit(str(self.widget.actions['pageEditable'].isChecked())))
        self.widget.actions['elementMask'].triggered.connect(lambda:self.elementMaskSignal.emit(str(self.widget.actions['elementMask'].isChecked())))
        self.refreshHtmlSignal.connect(self.refreshHtml)

    def initChannel(self):
        if not self.webChannel():
            self.channel = QtWebChannel.QWebChannel(self)
            self.setWebChannel(self.channel)
        else:
            self.channel = self.webChannel()
        self.channel.registerObjects(self.objs)

    def initObjs(self):
        self.objs['pyHtmlBrowserPage'] = self

    def initJS(self):
        # qwebchannel.js
        if not os.path.isfile(os.path.join(self.scriptPath, 'qwebchannel.js').replace('\\', '/')):
            raise Exception('File qwebchannel.js is not exist.')
        jsFile = open(os.path.join(self.scriptPath, 'qwebchannel.js').replace('\\', '/'), mode='r', encoding='utf-8')
        js = jsFile.read()
        jsFile.close()
        script = QtWebEngineWidgets.QWebEngineScript()
        script.setSourceCode(js)
        script.setName('qwebchannel')
        script.setWorldId(QtWebEngineWidgets.QWebEngineScript.MainWorld)
        script.setInjectionPoint(QtWebEngineWidgets.QWebEngineScript.DocumentCreation)
        script.setRunsOnSubFrames(False)
        self.scripts().insert(script)
        # addChannelObjs.js
        initialScript = ''
        endScript = ''
        for name, obj in self.objs.items():
            # initialScript += 'var {helper};\n'.format(helper=name)
            endScript += '\n{helper}=channel.objects.{helper};'.format(helper=name)
        js = initialScript + 'new QWebChannel(qt.webChannelTransport, function(channel) {' + endScript
        js +='''
            function disableClick(event) {
                event.preventDefault();
                window.onclick=function(event) {
                    pyHtmlBrowserPage.navigateBuffer(event.target.dataset.pyhtmlpos);
                }
            }

            function toggleElementMask(masked) {
                if(masked=="True") {
                    window.addEventListener('mouseover',applyMask);
                } else {
                    window.removeEventListener('mouseover',applyMask);
                    clearMasks()
                }
            }
            
            function toggleEditable(edit) {
                if(edit=="True") {
                    window.addEventListener("click", disableClick);
                    window.addEventListener('mouseout',moveOutWindow);
                } else {
                    window.removeEventListener("click", disableClick);
                    window.removeEventListener('mouseout',moveOutWindow);
                }
                
            }

            function moveOutWindow(event) {
                var from = event.relatedTarget || event.toElement;
                if (!from || from.nodeName == "HTML") {
                    clearMasks();
                }
                
            }

            function applyMask(event) {
                if(document.getElementsByClassName("highlight-wrap").length > 0) {
                    resizeMask(event.target);
                }else{
                    createMask(event.target);
                }
            }

            function resizeMask(target) {
                var rect = target.getBoundingClientRect();
                var hObj = document.getElementsByClassName('highlight-wrap')[0];
                hObj.style.top=rect.top+document.documentElement.scrollTop+"px";
                hObj.style.width=rect.width+"px";
                hObj.style.height=rect.height+"px";
                hObj.style.left=rect.left+document.documentElement.scrollLeft+"px";
                hObj.style.WebkitTransition='top 0.2s';
            }

            function createMask(target) {
                var rect = target.getBoundingClientRect();
                var hObj = document.createElement("div");
                hObj.className = 'highlight-wrap';
                hObj.style.position='absolute';
                hObj.style.top=rect.top+"px";
                hObj.style.width=rect.width+"px";
                hObj.style.height=rect.height+"px";
                hObj.style.left=rect.left+"px";
                hObj.style.backgroundColor = '#205081';
                hObj.style.opacity='0.5';
                hObj.style.cursor='default';
                hObj.style.WebkitTransition='top 0.2s';
                hObj.style.pointerEvents='none';
                hObj.style.zIndex='9999';
                //target.appendChild(hObj);
                document.body.appendChild(hObj);
            }

            function clearMasks() {
                var hwrappersLength = document.getElementsByClassName("highlight-wrap").length;
                var hwrappers = document.getElementsByClassName("highlight-wrap");
                if(hwrappersLength > 0) {
                    for(var i=0; i<hwrappersLength; i++) {
                        console.log("Removing existing wrap");
                        hwrappers[i].remove();
                    }
                }
            }
            pyHtmlBrowserPage.pageEditableSignal.connect(toggleEditable);
            pyHtmlBrowserPage.elementMaskSignal.connect(toggleElementMask);
        '''
        js += '\n});\n'
        script = QtWebEngineWidgets.QWebEngineScript()
        script.setSourceCode(js)
        script.setName('addChannelObjs')
        script.setWorldId(QtWebEngineWidgets.QWebEngineScript.MainWorld)
        script.setInjectionPoint(QtWebEngineWidgets.QWebEngineScript.DocumentReady)
        script.setRunsOnSubFrames(False)
        self.scripts().insert(script)

    def onLoadFinished(self):
        self.pageEditableSignal.emit(str(self.widget.actions['pageEditable'].isChecked()))

    @QtCore.Slot(str)
    def navigateBuffer(self, tagPos=None):
        if tagPos and re.match(r'\d+,\d+', tagPos):
            vim.current.window.cursor= [int(i) for i in tagPos.split(',')]
            vim.command('call foreground()')
            if vim.eval('g:pyHtmlPreviewerAutoOpenFold'):
                vim.command('normal zv')

    def refreshHtml(self, target='VIM', p=None):
        # target in VIM, BROWSER, BOTH
        if target == 'VIM' or target == 'BOTH' and p:
            vim.command('e {filePath}'.format(filePath=p))
        if target == 'BROWSER' or target == 'BOTH':
            self.html = '\n'.join(vim.current.buffer)
            op=int(vim.eval('g:pyHtmlPreviewerPosAtText'))
            parser = pyHtmlParser(op)
            parser.feed(self.html)
            self.html = parser.data
            vim.command("let s:bufferPath=expand('%:p:h')")
            bufferPath = vim.eval('s:bufferPath').replace('\\', '/') + '/'
            self.setHtml(self.html, baseUrl=QtCore.QUrl('file:///'+bufferPath))

    def acceptNavigationRequest(self, url, tp, isMainFrame):
        if tp in (QtWebEngineWidgets.QWebEnginePage.NavigationTypeLinkClicked, QtWebEngineWidgets.QWebEnginePage.NavigationTypeReload, QtWebEngineWidgets.QWebEnginePage.NavigationTypeOther):
            self.refreshHtmlSignal.emit('VIM', url.toLocalFile())
        if tp == QtWebEngineWidgets.QWebEnginePage.NavigationTypeRedirect:
            self.refreshHtmlSignal.emit('VIM', url.toLocalFile())
        if tp == QtWebEngineWidgets.QWebEnginePage.NavigationTypeBackForward:
            bkitem = self.history().backItem()
            self.history().back()
            self.refreshHtmlSignal.emit('VIM', bkitem.url().toLocalFile())
        return True

    def javaScriptConsoleMessage(self, level, message, lineNum, sID):
        return
        f = open(os.path.join(self.scriptPath, 'js.log'), 'a')
        print("JS: {} {}: {}".format(lineNum, sID, message), file=f)
        f.close()
